package moon.core.expression;

import java.util.ArrayList;
import java.util.List;

import moon.core.Model;
import moon.core.classdef.ClassType;
import moon.core.classdef.Type;
import moon.core.entity.Entity;
import moon.core.genericity.BoundS;
import moon.core.genericity.FormalPar;

import org.apache.log4j.Logger;

/**
 * 
 * Expression with length 1 calling an entity.
 * 
 * Used with a method it should be a function returning an entity.
 * 
 * @see moon.core.expression.Expr
 * @see moon.core.expression.CallExpr
 * @see moon.core.entity.Entity
 * @see moon.core.classdef.MethDec
 */
@SuppressWarnings("serial")
public abstract class CallExpr extends Expr {

	/**
	 * Logger.
	 */
	public static Logger logger = Logger
			.getLogger(CallExpr.class);
	
	/**
	 * Left side of the expression. For example, a.b, left side of b is a.
	 */
	private Expr leftSide;

	/**
	 * Right side of the expression. For example, a.b, right side of a is b.
	 */
	private Expr rightSide;

	/**
	 * First entity that uses the second entity.
	 */
	private Entity firstElement;

	/**
	 * Real arguments. When an entity is obtained using a function call we use a
	 * set of real arguments, atomic expressions.
	 * 
	 * @see moon.core.entity.FunctionDec
	 * @see moon.core.expression.Expr
	 */
	private List<Expr> realArguments;

	/**
	 * Constructor.
	 * 
	 * @param entity
	 *            entity
	 */
	public CallExpr(Entity entity) {

		super(entity.getType());
		firstElement = entity;
		realArguments = new ArrayList<Expr>();
	}

	/**
	 * Constructor.
	 * 
	 * @param entity
	 *            entity
	 * @param list
	 *            real arguments
	 */
	public CallExpr(Entity entity, List<Expr> list) {
		super(entity.getType());
		firstElement = entity;
		realArguments = list;
	}

	/**
	 * Gets the first element.
	 * 
	 * @return entity first entity
	 * @see #setFirstElement
	 */
	public Entity getFirstElement() {
		return this.firstElement;
	}

	/**
	 * Sets the first element.
	 * 
	 * @param pFirstElement
	 *            entity
	 * @see #getFirstElement
	 */
	public void setFirstElement(Entity pFirstElement) {
		this.firstElement = pFirstElement;
	}

	/**
	 * Adds an argument to the list of real arguments.
	 * 
	 * @param pExprAtom
	 *            real argument
	 * @see #getRealArgument
	 */
	public void setRealArgument(Expr pExprAtom) {
		this.realArguments.add(pExprAtom);
	}

	/**
	 * Gets the real argument in certain position.
	 * 
	 * @param index
	 *            position
	 * @return atomic expression in that position
	 * @see #setRealArgument
	 */
	public Expr getRealArgument(int index) {
		return realArguments.get(index);
	}

	/**
	 * Gets the real arguments.
	 * 
	 * @return real arguments
	 */
	public List<Expr> getRealArguments() {
		return realArguments;
	}

	/**
	 * Gets the left side if exists.
	 * 
	 * @return left side or null
	 * @see #setLeftSide
	 */
	public Expr getLeftSide() {
		return leftSide;
	}

	/**
	 * Gets the right side if exists.
	 * 
	 * @return left side or null
	 * @see #setRightSide
	 */
	public Expr getRightSide() {
		return rightSide;
	}

	/**
	 * Sets the left side.
	 * 
	 * @param jcel1
	 *            left side
	 * @see #getLeftSide
	 */
	public void setLeftSide(Expr jcel1) {
		this.leftSide = jcel1;
	}

	/**
	 * Sets the right side.
	 * 
	 * @param jcel1
	 *            right side
	 * @see #getRightSide
	 */
	public void setRightSide(Expr jcel1) {
		this.rightSide = jcel1;
	}

	/**
	 * {@inheritDoc}.
	 * 
	 * @return {@inheritDoc}
	 */
	@Override
	public boolean isConstant() {
		return false;
	}

	/**
	 * {@inheritDoc}.
	 * 
	 * @return {@inheritDoc}
	 */
	@Override
	public boolean isCreation() {
		return false;
	}

	/**
	 * {@inheritDoc}. Gets the type of the entity.
	 * 
	 * @return {@inheritDoc}
	 */
	@Override
	public Type getType() {
		if (this.hasCastType()) {
			return this.getCastType();
		}

		Type result = this.firstElement.getType();
		
		if (!result.isComplete()) { // i.e. E, List<E>, Map<K,V>
			logger.warn("not complete " + result.getUniqueName().toString());
			Expr leftSide = this.getLeftSide();
			
			if (leftSide!=null){
				logger.warn("leftSide.getType():" + leftSide.getType().getUniqueName().toString());
				logger.warn("left side is complete:" + leftSide.getType().isComplete());
				logger.warn("left side is generic:" + leftSide.getType().getClassDef().isGeneric());
			}
			
			if (leftSide != null && leftSide.getType().isComplete()
					&& leftSide.getType().getClassDef().isGeneric()) {
				
				result = infer(leftSide.getType(), result);
				//logger.warn("Inferred type:" + result.getTypeCode());
				if (result == null) {
					result = this.firstElement.getType();
				}
			}
			logger.warn("type result:"  + result.getUniqueName().toString());
		}

		return result;
	}

	/**
	 * Infer the type using a method with generic formal parameters.
	 * 
	 * i.e List l = new ArrayList(); l.add("text"); String s = l.get(0); should
	 * infer String for <E> in List.
	 * 
	 * @param leftTypeComplete
	 *            left side type
	 * @param result
	 *            result of the invocation (using formal parameters)
	 * @return complete type
	 * 
	 */
	private Type infer(Type leftTypeComplete, Type result) {
		Type newType = null;
		ClassType classType = (ClassType) leftTypeComplete;

		//System.out.println("Call expr infer type: " + this.toString());
		//System.out.println("Left type complete: " 
		//		+ leftTypeComplete.getUniqueName().toString());
		//System.out.println("Type result: " 
		//		+ result.getUniqueName().toString());
		
		List<FormalPar> formalParameters = leftTypeComplete.getClassDef().getFormalPars();
		//logger.warn("Formal Parameters related: " + formalParameters.toString());
		List<Type> substitutions = getSubstitutions(leftTypeComplete,
				formalParameters);
		//logger.warn("Substitutions: " + substitutions.toString());
				
		if (substitutions.size()==0) {
			// universal object type
			if (result instanceof ClassType && ((ClassType)result).isParametricType()){
				// Raw type
				newType = result;	
			}
			else if (result instanceof BoundS) {
				// search the first bound not deferred
				List<Type> boundList = ((BoundS) result).getBounds();
				for (Type bound : boundList) {
					if (!bound.getClassDef().isDeferred()){
						newType = bound;
						break;
					}
				}
			}
			else {
				// Object
				newType = result.getClassDef().getNameSpace().getModel().getUniversalObject().getClassType();
			}
		} // substitutions size is zero
		else if (result.getClassDef()!=null 
				&& result.getClassDef().isGeneric() && !(result instanceof FormalPar)) {
			//logger.warn("4");
			// remove from substitutions non necessary substitutions...
			List<Type> newSubstitutions = 
				filterSubstitutions(formalParameters, substitutions, (ClassType) result);

			//System.out.println("New substitutions: " + newSubstitutions);
			
			Model model = result.getClassDef().getNameSpace().getModel();
			newType = model.getMoonFactory().createParametricType(
					result.getClassDef().getName(), result.getClassDef(),
					newSubstitutions);		
			
		} else if (substitutions.size()>0){
			//logger.warn("5");
			if (result instanceof FormalPar) {
				//logger.warn("instanceof formalpar");
				int pos = 0;
				if (((FormalPar) result).getClassDef()!=null){
					pos = ((FormalPar) result).getClassDef().getFormalParIndex(result.getName());
				}
				//logger.warn("Pos: " + pos);
				if (substitutions.size()>pos){			
					newType = substitutions.get(pos);
				}
				else {					
					newType = substitutions.get(0);
				}
			}
			else {				
				newType = substitutions.get(0);
			}
		}
		//logger.warn("type to return: " + newType.getUniqueName().toString());
		return newType;
	}

	/**
	 * Gets the substitutions of the formal parameters. i.e. A<C> with E return
	 * C
	 * 
	 * @param leftTypeComplete
	 *            parametric type
	 * @param formalParameters
	 *            formal parameters
	 * @return substitutions
	 */
	public List<Type> getSubstitutions(Type leftTypeComplete,
			List<FormalPar> formalParameters) {

		List<Type> substitutions = new ArrayList<Type>();
		List<Type> realArguments = ((ClassType) leftTypeComplete)
				.getRealParameters();
		//System.out.println(this.toString());
		//System.out.println("Left type complete: "
		//		+ leftTypeComplete.getUniqueName().toString());
		//System.out.println("Formal parameters: " + formalParameters);
		//System.out.println("Real arguments: " + realArguments);
		if (realArguments.size() == formalParameters.size()) {
			for (FormalPar fp : formalParameters) {
				int index = -1;
				if (fp.getClassDef()!=null)  { // formal par in class
					index = fp.getClassDef().getFormalParIndex(fp.getName());
				}
				else { // formal par in method
					index = fp.getMethDec().getFormalParIndex(fp.getName());
				}				
				substitutions.add(realArguments.get(index));
			}
		}
		return substitutions;
	}
	
	
	private List<Type> filterSubstitutions(List<FormalPar> formalParameters, List<Type> substitutions,  ClassType result) {
		List<Type> list = new ArrayList<Type>();
		List<Type> realParameters = result.getRealParameters();
		
		for (Type type : realParameters) {
			for (FormalPar fp : formalParameters) {
				if (type instanceof FormalPar && type.getName().equals(fp.getName())){
					int index = fp.getClassDef().getFormalParIndex(fp.getName());
					list.add(substitutions.get(index));
				}
			}
		}
		return list;
	}
} // CallExpr
